11  Preprocesamiento de textos

11.1 Introducción

Una vez tengamos los archivos descargados y guardados en la nube o en una carpeta en el disco duro de nuestro ordenador, podemos ir un paso más allá para abrirlos en R, extraer información y limpiarlos si resulta necesario. En esta sección, veremos cómo abrir archivos con distintos formatos (txt, PDF, docx entre otros) y extraer texto de PDFs sin tratar y limpiar los archivos antes de pasar a la siguiente fase de organización de los corpora.

Este pequeño apartado tiene tres secciones. La primera abre archivos de texto en R que serán luego empleados para la creación de un todo coherente, relativamente comparable, que se someterá al análisis (corpus). La segunda realiza un OCR en archivos en formato PDF para, luego, extraer el texto. Finalmente, la tercera utiliza un conjunto de funciones de para la limpieza y búsqueda sistemática de texto, así como introduce las expresiones regulares.

11.2 Abrir archivos de textos

El primer paso de cualquier análisis de texto consiste en abrir los textos en el R para su posterior procesamiento y análisis. Afortunadamente, existe una serie de opciones que facilitan mucho la apertura de una cantidad grande de textos de un solo golpe, sin la necesidad de ir de uno en uno.

La función readtext() del paquete homónimo lee desde archivos de textos a PDFs, documentos de Word y otros formatos como planillas de Excel o json. No solo lee un archivo de cada vez, sino todavía mejor. Basta con suministrar el camino hasta la carpeta y la función trata de importar todos los archivos ahí contenidos de un golpe.

En la sección sobre web scraping hemos bajado el texto completo de más de 800 libros en español disponibles en los servidores del Proyecto Gutenberg. Los hemos guardado todos en la carpeta “Gut_txt/Archivos/”. Ahora podemos abrirlos de una vez en R utilizando la función readtext(). Veamos cómo se hace:

Código
# Abre el paquete readtext
library(readtext)
library(reactable)

# Abre todos los archivos
gt <- readtext("../Text_Classify/Data/Source/Scraping/Gut_txt/Archivos/")

# Visualiza los primeros 10
reactable(gt[1:10,], wrap=F, resizable = T)


El mismo procedimiento se puede llevar a cabo con archivos PDF que ya han sido sometidos a un OCR o desde un primer momento son digitales. Como vemos abajo, el código es exactamente el mismo. Lo único que cambia es la dirección de la carpeta, que en esta ocasión contiene solamente archivos PDF:

Código
# Carga los paquetes
library(readtext)
library(reactable)

# Extrae los textos de todos los archivos en la carpeta
gt <- readtext("../Text_Classify/Data/Source/Scraping/PDFs/")

# Visualiza los 10 primeros
reactable(gt[1:10,], wrap=F, resizable = T)


Como en el caso anterior, el R genera un data.frame con dos variables: doc_id, conteniendo el nombre del archivo, y text con el texto completo. Este nuevo objeto será utilizado luego para la creación de objeto de tipo corpus en la tercera parte de esta sección.

11.3 OCR y extracción de texto

Sin embargo, el mundo sería un lugar más aburrido si las cosas siempre fueran tan sencillas. En muchos casos, nos encontraremos con archivos PDF escaneados con una resolución baja y sin reconocimiento de caracteres. En estos casos, nos vemos forzados a procesar los archivos antes de poder llevar a cabo cualquier análisis.

En R, el paquete tesseract permite realizar el reconocimiento óptico de caracteres (OCR, en su acrónimo original en inglés) en múltiples archivos y en distintas lenguas. Combinado con el paquete pdftools, permiten extraer el texto desde fuentes difíciles de tratar.

Utilizaremos los mismos PDFs para realizar el OCR y luego extraer los textos. El análisis se dividirá en dos partes. En la primera, generaremos una lista de los archivos a ser procesados y descargaremos el modelo de OCR para español.

Código
# Carga los paquetes
library(tesseract)
library(pdftools)

# Genera la lista de todos los PDFs
fl <- list.files("../Text_Classify/Data/Source/Scraping/PDFs/")

# Baja el modelo para realizar el 
# OCR en espaniol (solo una vez)
tesseract_download("spa")

# Establece el espaniol como 
# lengua para el OCR
esp <- tesseract("spa")

En la segunda parte, utilizaremos un bucle for para ir de archivo en archivo, realizar el OCR, extraer el texto y guardarlo en un nuevo formato (.txt) en una nueva carpeta.

Código
# Para cada PDF
for (i in 1:length(fl)){
  
  # Informa el avace
  print(paste0(i, " of ", length(fl)))
  
  # Extrae el nombre del archivo
  ls <- unlist(strsplit(fl[i],"/"))
  ls <- gsub(".pdf","", ls)
  ls <- ls[length(ls)]
  
  # Realiza el OCR
  text <- tesseract::ocr(
    paste0("../Text_Classify/Data/Source/Scraping/PDFs/",fl[i]), 
    engine = esp)
  
  # Guarda el resultado en formato texto 
  write(text, paste0("../Text_Classify/Data/Source/Scraping/PDFs/OCR_txt/",ls,".txt"))
  
}

Ahora podemos averiguar los resultados obtenidos por medio de la función readtext() utilizando el mismo código que hemos visto antes:

Código
# Carga los paquetes
library(readtext)
library(reactable)

# Extrae los textos de todos los archivos en la carpeta
gt <- readtext("../Text_Classify/Data/Source/Scraping/PDFs/OCR_txt/")

# Visualiza los 10 primeros
reactable(gt[1:10,], wrap=F, resizable = T)

11.4 Manipulación y limpieza de textos

La limpieza de los datos resulta fundamental para obtener un análisis adecuado de los textos. Se trata de un proceso laborioso, pero muy importante para la obtención de datos comparables. Aúna un conjunto de tareas concretas de manipulación que incluye: remover espacios en blanco, tildes, saltos de línea innecesarios o la extracción de datos o metadatos.

Lo que veremos aquí es un conjunto de técnicas que se pueden adaptar a textos de distinta estructura y naturaleza. No existe una solución universal de tratamiento de datos que funcione igual para tweets o para textos legales. En el caso de los primeros, habrá que tratar los elementos no textuales o los “emojis” antes de analizar el contenido. En los segundos, suele haber mucho ruido por la repetición de los encabezados de página por su publicación en archivos PDF.

Además, cada estructura nos brindará oportunidades distintas de extracción y análisis de los datos. Por ejemplo, textos legales suelen ser muy estructurados y contienen la identificación de actores, títulos, capítulos, etc. Podemos utilizar tales informaciones como “marcadores” o “etiquetas” a la hora de extraer datos de forma sistemática. Por esa razón, resulta muy útil empezar por la sencilla tarea de explorar y describir cuál es la estructura del texto. ¿Se trata de un texto uniforme o segmentado (divisiones de capítulos, partes, títulos, artículos o cualquier otra)? ¿El texto tiene un formato digital desde el principio o tenemos que tratar encabezados u otros elementos comunes en PDFs y documentos Word? ¿El texto abre con todas las letras legibles o aparecen símblos raros en las tildes? O sea, ¿está en la codificación de caracteres adecuada o tengo que abrirlo utilizando una codificación específica (“LATIN1” es la más común para los que trabajamos textos en español)? La función stri_enc_list() del paquete stringi proporciona un listado completo de las codificaciones.

En esta parte del laboratorio, veremos algunas técnicas de manipulación de textos que permiten prepararlos para el análisis. Dividiremos el contenido en tres secciones. La primera examina las funciones de manipulación de texto de R y de los paquetes stringr y stringi. La segunda introduce brevemente las expresiones regulares, que representan un recurso muy útil para la identificación de patrones en textos. Finalmente, la tercera aplica el contenido de las dos anteriores en los textos que empleamos de ejemplo: los libros en español del Proyecto Gutenmberg y los decretos presidenciales de Paraguay.

11.4.1 Cuenta, busca, extrae, divide, combina, sustituye, compara

Existe un número amplio de funciones en R para la manipulación de texto. Podemos hacer casi cualquier operación desde buscar expresiones concretas hasta combinar textos o transformarlos en otras estructuras. Aquí exploraremos algunas tareas básicas muy útiles para trabajar con textos en R.

11.4.1.1 Cuenta

Una tarea de análisis de texto consiste en contar las veces que determinados temas, contenidos o conceptos aparecen. Esto se puede hacer utilizando ciertas palabras o diccionarios que ayudan a definir el peso de un tópico en el conjunto de elementos de un texto.

Por ejemplo, ¿cuántas veces aparecen palabras que empiezan con “demo” en una variable? La función stri_count() del paquete stringi retorna el número de texto que un patrón cualquiera (en nuestro caso “demo”) aparece en un texto o en una variable.

Código
# Crea una variable de texto
tx <- "La democracia es la forma de gobierno originada a partir del demos, o pueblo."

# Carga el paquete stringr
library(stringi)

# Cuenta las palabras que contienen "demo"
stri_count(tx, regex = "demo")
[1] 2
Código
# Ahora con una variable
tx <- c("democracia","demostenes democrático","nada","demora")

# Cuenta las palabras que contienen "demo"
# para cada elemento
stri_count(tx, regex = "demo")
[1] 1 2 0 1

Como vemos, en el primer caso, el R nos ha retornado las dos veces en las que alguna palabra conteniendo “demo” aparecía en la frase. En el segundo, dos elementos llaman la atención. Primero, ya no es el total de veces en general, sino que el número se divide por observación de la variable. Segundo, debemos tener cuidado con la raíz que utilizamos para evitar ambiguedades y generar falsos positivos. Por ejemplo, demostenes y demora no tienen ninguna relación con democracia.

Otra forma de contar que puede ser útil en algunos procesos de manipulación de texto. Por ejemplo, los códigos INE de los municipios de España incluyen dos caracteres iniciales con el código de la provincia y luego tres caracteres con el orden alfabético del municipio. Así que Almería tiene el código “04” y está en el 13º puesto en orden alfabético. No obstante, muchas veces, ciertas agencias informan el código como “04013” mientras otras lo informan como “4013”. Sin tratamiento, esto resulta un problema a la hora de comparar los datos.

La función stri_length() del paquete stringi soluciona el problema al contar cuántos caracteres hay en cada observación de una variable de texto. A partir de ese dato, podemos identificar cuáles elementos debemos tratar. En el ejemplo abajo añadimos un 0 al texto solo para aquellos códigos que son menores de 5 caracteres. De ese modo, uniformizamos el sistema de acuerdo con el estándar definido por el INE:

Código
# Crea una variable con los códigos INE para los municipios de
# Almería, Barcelona, Madrid, Salamanca y Zamora
tx <- c("4013","8019","28079","37274","49275")

# Carga el paquete
library(stringi)

# Cuenta los caracteres
stri_length(tx)
[1] 4 4 5 5 5
Código
# Incluye un cero en el codigo del municipio
tx[stri_length(tx)<5] <- paste0("0", tx[stri_length(tx)<5]) 

# Inspecciona los resultados
tx
[1] "04013" "08019" "28079" "37274" "49275"

11.4.1.2 Busca

En otras ocasiones, lo que deseamos es saber cuáles elementos del texto contienen ciertas ideas o palabras-clave que buscamos. En ese caso, se trata de identificar o si dichas expresiones se encuentran o no en el texto o, al reves, aquellos textos que contienen la palabra.

Por ejemplo, ¿cuáles elementos de una variable contienen la palabra ministerio o ministro? La función stri_detect() del paquete stringi lleva a cabo dicha tarea.

Código
# Crea una variable con distinto contenido
tx <- c("ministro de telecomunicaciones",
        "secretaria adjunto de la presidencia", 
        "ministerio de agricultura", 
        "director de la policía nacional",
        "ministerio de seguridad social",
        "ministra de educación",
        "secretaría nacional de derechos humanos",
        "directoría de asuntos exteriores")

# Carga el paquete sgtringi
library(stringi)

# Detecta cuáles elementos contienen ministro o ministerio
stri_detect(tx, regex = "minist")
[1]  TRUE FALSE  TRUE FALSE  TRUE  TRUE FALSE FALSE
Código
# Podemos seleccionarlos si queremos
tx[stri_detect(tx, regex = "minist")]
[1] "ministro de telecomunicaciones" "ministerio de agricultura"     
[3] "ministerio de seguridad social" "ministra de educación"         

Como se puede observar, el R retorna los elementos de la variable que contienen el patrón “minist”. En la primera forma es solamente una indicando TRUE o FALSE. En la segunda, hemos pedido que nos regrese el texto completo de cada observación.

Ejercicio: Podéis ejercitar el nuevo conocimiento intentando buscar “secretario” o “secretaría” y “director” o “diretoría”.

11.4.1.3 Extrae

En otras ocasiones, queremos extraer los patrones para, por ejemplo, contar el número de veces que ocurren. En el siguiente ejemplo, extraeremos del discurso de investidura de Pedro Sánchez todas las palabras empezadas por igual (igualdad, igualitario, etc.) y por libert (libertad, libertades). Esto es posible gracias a la función stri_extract_all() del paquete stringi.

Código
# Carga el paquete readtext
library(readtext)

# Lee el discurso de investidura de Pedro Sánchez de 2020
tx <- readtext("../Text_Classify/Data/Source/Scraping/Otros/Discursos_Presidentes/Espana/15_XIV_Leg_Sanchez.txt",encoding ="LATIN1")

# Carga el paquete stringi
library(stringi)

# Extrae las palabras con raiz igual
fr <- unlist(stri_extract_all(tx, regex = "igual[a-z]+"))

# Cuenta la frecuencia
table(fr)
fr
  igualdad igualdades    iguales igualmente 
        26          2          2          1 
Código
# Extrae las palabras con raiz libert
fr <- unlist(stri_extract_all(tx, regex = "libert[a-z]+"))

# Cuenta la frecuencia
table(fr)
fr
  libertad libertades 
        15          3 

Se ve como hay una frecuencia mayor de palabras relacionadas a la igualdad que a la libertad, aunque estas últimas también estén presentes en una proporción no muy inferior.

11.4.1.4 Divide

Otra tarea de manipulación de textos consiste en dividirlos según diferentes criterios que requieren cada análisis. Por ejemplo, una tarea muy común consiste en fragmentar los textos en palabras, algo que se denomina tokenization. Hay dos funciones en el paquete stringi que nos permiten dividir un texto: stri_split(), que utiliza un patrón para dividirlo y stri_split_fixed() que limita el número de fragmentos. Veamos un ejemplo:

Código
# Crea una variable de texto
tx <- "Pepi, Luci, Bom y otras chicas del montón."

# Carga el paquete stringr
library(stringi)

# Separa utilizando la coma
stri_split(tx, regex =",")
[[1]]
[1] "Pepi"                            " Luci"                          
[3] " Bom y otras chicas del montón."
Código
# Separa utilizando la coma, pero en solo dos fragmentos
stri_split_fixed(str = tx, pattern = ", ", n = 2)
[[1]]
[1] "Pepi"                                
[2] "Luci, Bom y otras chicas del montón."
Código
# Un poco más avanzado - separa utilizando tanto la coma
# como la y
stri_split(tx, regex ="[,y]")
[[1]]
[1] "Pepi"                      " Luci"                    
[3] " Bom "                     " otras chicas del montón."

11.4.1.5 Combina

En algunas ocasiones, necesitamos combinar distintos textos para trabajar con términos compuestos, bigramas o cualquier otra finalidad. El código abajo nos enseña cómo hacerlo utilizando la función stri_join() del paquete stringi.

Código
# Crea una variable de cantidades
val <- c(1, 2, 3, 4)

# Crea una variable de texto
tx <- c("coche", "bicicletas", "hijos", "libros")

# Carga el paquete stringr
library(stringi)

# Combina los dos textos 
stri_join(val, tx, sep=" ", collapse=", ")
[1] "1 coche, 2 bicicletas, 3 hijos, 4 libros"

11.4.1.6 Sustituye

La sustitución resulta muy útil para trabajar textos cuando se require el reemplazo de un valor por otro. Imaginemos que eres profesor y tienes una clase de 130 estudiantes. Como eres atento, les enviarás un informe con las notas por correo electrónico. Ya tienes un archivo Excel con sus nombres y calificaciones. No obstante, resulta muy trabajoso escribir a cada uno copiando y pegando el mismo texto.

La función stri_replace, del paquete que ya conocéis, permite reemplazar los datos como nombre y nota y facilitar el trabajo de redacción. Luego, se pueden utilizar otros paquetes como el gmailr para enviar los correos de forma automatizada (este último paso no lo haremos aquí).

Código
# Crea una variable de texto
tx <- "EstimadART NOMBRE,\n\nEspero que este correo le encuentre bien.\n\nComo prometido, envío la calificación de la asignatura.\nSu nota final ha sido NOTA.EMOJI\n\nReciba un cordial saludo,\n\nRodrigo\n\n"

# Pongamos unos emojis solo para divertirnos.
emo <- c("\U1F937","\U1F64C","\U1F44D","\U1F947")

# Crea una lista de nombres
nm <- c("Pepe", "Manuel","María","Lola")

# Lista de notas
nota <- c(0, 5, 7.5, 8)

# Artículo definido
art <- c("o","o","a","a")

# Carga el paquete stringr
library(stringi)

# Reemplaza el nombre
st <- stri_replace(tx, nm, regex ="NOMBRE")

# Reemplaza el artículo definido
st <- stri_replace(st, art, regex ="ART")

# Reemplaza el emoji
st <- stri_replace(st, emo, regex ="EMOJI")

# Ahora reemplaza la nota
st <- stri_replace(st, nota, regex ="NOTA")

# Imprime los resultados
cat(st)
Estimado Pepe,

Espero que este correo le encuentre bien.

Como prometido, envío la calificación de la asignatura.
Su nota final ha sido 0.🤷

Reciba un cordial saludo,

Rodrigo

 Estimado Manuel,

Espero que este correo le encuentre bien.

Como prometido, envío la calificación de la asignatura.
Su nota final ha sido 5.🙌

Reciba un cordial saludo,

Rodrigo

 Estimada María,

Espero que este correo le encuentre bien.

Como prometido, envío la calificación de la asignatura.
Su nota final ha sido 7.5.👍

Reciba un cordial saludo,

Rodrigo

 Estimada Lola,

Espero que este correo le encuentre bien.

Como prometido, envío la calificación de la asignatura.
Su nota final ha sido 8.🥇

Reciba un cordial saludo,

Rodrigo

11.4.1.7 Compara

Otra tarea muy útil consiste en comparar textos y determinar su similitud. Imaginemos que comparamos direcciones, o nombres de personas o entidades en fuentes que pueden contener errores ortográficos o de digitación. En esos casos, resulta fundamental poder medir el grado de similitud o diferencia para tomar una decisión sobre si se trata de la misma entidad o no.

El paquete stringdist posee diversas funciones orientadas a esta finalidad. Que permiten comparar desde dos textos entre sí hasta múltiple textos entre ellos.

Ilustremos cómo hacerlo utilizando dos textos literarios. En junio de 1580 muere en Lisboa el poeta Luis de Camões. En septiembre de este mismo año, Madrid asiste a la llegada al mundo de otro inmenso escritor, Francisco de Quevedo. Cualquier nativo o hablante fluyente de portugués o español no puede dejar de sorprenderse por la similitud entre dos sonetos de ambos autores sobre el amor. Incluso, en algunas estrofas, la redacción es idéntica.

El objetivo del código abajo resulta comparar ambos sonetos, estrofa por estrofa, y determinar el grado de similitud entre ellas. Nos restringiremos aquí solamente a algoritmos de similitud que comparan palabras sin atenernos a su función sintáctica o la carga semántica que conlleva. Por lo tanto, se trata de un análisis sencillo de la estructura de las estrofas.

Código
# Soneto del amor (Luis de Camões, 1598 - póstumo)
cam1598 <- c("amor es fuego que arde sin verse",
            "es herida que duele y no se siente",
            "es un contentamiento descontento",
            "es dolor que lastima sin doler",
            "es un no querer mas que bien querer",
            "es andar solitario entre la gente",
            "es nunca contentarse de contento",
            "es un cuidar que gana en perderse",
            "es querer estar aprisionado por voluntad",
            "es servir a quien vence, el vencedor",
            "es tener con quien nos mata, lealtad",
            "pero cómo causar puede su favor",
            "en los corazones humanos amistad",
            "si tan contrario a si mismo es el amor")
  
# Soneto del amor (Francisco de Quevedo, 1670 - póstumo)  
qev1670 <- c("es yelo abrasador, es fuego helado", 
            "es herida que duele y no se siente",
            "es un soñado bien, un mal presente",
            "es un breve descanso muy cansado",
            "es un descuido que nos da cuidado",
            "un cobarde, con nombre de valiente",
            "un andar solitario entre la gente",
            "un amar solamente ser amado",
            "es una libertad encarcelada",
            "que dura hasta el postrero parasismo",
            "enfermedad que crece si es curada",
            "este es el nino amor, este es su abismo",
            "mirad cual amistad tendra con nada",
            "el que en todo es contrario de si mismo")  


# Carga los paquetes stringdist - para calcular la similitud 
# y reshape2 - para cambiar el formato de un data.frame
library(stringdist)
library(reshape2)

# Calcula la matriz de similitud entre los dos textos
# La matriz permitirá identificar estrofas incluso si
# se ha cambiado el orden.
rd <- round(stringsimmatrix(cam1598, 
                            qev1670, 
                            method = "lcs"),2)

# Establece los nombres del las lineas como de Camões
# y el nombre de las columnas como de Quevedo
rownames(rd) <- cam1598
colnames(rd) <- qev1670

# Transforma la matriz en un data.frame
drd <- melt(rd)

# Da nombre a las variables
names(drd) <- c("Camoes","Quevedo","Similitud")

# Selecciona solamente los resultados cuya 
# similitud resulta superior a 50%.
drd <- drd[drd$Similitud>0.5,]

# Ordena las estrofas restantes de la más
# similar a la menos
drd <- drd[order(drd$Similitud, decreasing = T),]

# Inspecciona los resultados
reactable(data = drd, resizable = T, striped = T)


Se puede ver que, al menos cuatro estrofas de las 14 (28,6%) son muy similares. Resulta claro que esos dos textos presentan un fuerte parentesco e indican que Quevedo ha sido lector de Camões. Este mismo método puede ser aplicado para cualquier otro tipo de texto. El aspecto crucial es la elección de la unidad de comparación básica. En este ejemplo, la estrofa se empleó como unidad de análisis. En otras fuentes quizás párrafos o cuasi-frases sean las más indicadas. Siempre hay que explorar diferentes posibilidades y métodos antes de aplicar un algoritmo a un número amplio de casos.

11.4.2 Expresiones regulares

Las expresiones regulares son formas de sintaxis que permiten encontrar patrones en textos. Resultan tremendamente útiles a la hora de eliminar espacios en blanco, remover puntuación o acentos. Permite, además, encontrar palabras o números según patrones concretos. Su uso nos facilita buscar información, eliminar secciones que no nos sirven y evitar errores.

Por ejemplo, el código abajo remueve los dobles espacios en blanco del texto:

Código
# Crea una variable con muchos espacios
tx <- "Este    texto      tiene    muchos espacios en      blanco."

# Sustituye los múltiples espacios por solo uno 
gsub("\\s+"," ", tx)
[1] "Este texto tiene muchos espacios en blanco."
Código
# Sustituye dos espacios por uno 
gsub("\\s{2}"," ", tx)
[1] "Este  texto   tiene  muchos espacios en   blanco."

La función gsub() sirve para reemplazar textos en una variable. En el primer ejemplo, la expresión regular \\s+ indica al R que busque cualquier secuencia de texto en la que haya un espacio en blanco o más y la reemplaza por solo un espacio. En la segunda, \\s{2} busca dos espacios y los sustituye por uno. Como vemos, los resultados son distintos porque hemos solicitado que R hiciera búsquedas diferentes.

Imaginemos que hay una variable de texto y necesitamos encontrar todos los números contenidos en ella. La función str_extract_all() del paquete stringi permite extraer información de una variable de texto. Si la combinamos con la expresión regular \\d+ (dígitos numéricos), el resultado es un conjunto de números.

Código
# Crea una variable textos conteniendo números
tx <- c("Tengo 10 euros y debo 1000.",
        "De los 18 equipos, sono 1 puede llegar a campeón.", 
        "Más vale 8 que 80.")

# Carga el paquete
library(stringi)

# Extrae los números 
stri_extract_all(tx, regex = "\\d+")
[[1]]
[1] "10"   "1000"

[[2]]
[1] "18" "1" 

[[3]]
[1] "8"  "80"

En el ejemplo abajo, se utiliza otra expresión regular [A-Z] (mayúsculas), luego *[a-z]** (seguida de minúsculas) para encontrar y extraer las palabras iniciadas en mayúsculas en el texto.

Código
# Carga el paquete
library(stringi)

# Crea un texto de ejemplo
tx <- "Aqui pondremos algunos Ministerios, la Presidencia y el presidente."

## Extrae del texto expresiones con mayusculas
stri_extract_all(tx,regex = "[A-Z][a-z]*")
[[1]]
[1] "Aqui"        "Ministerios" "Presidencia"

También se puede dividir un texto utilizando un caracter o palabra. La función stri_split() fragmenta una variable de texto a partir de un patrón que puede ser un caracter, como un espacio, una palabra, o un símbolo, como el de salto de linea.

Código
# Carga el paquete
library(stringi)

## Define el texto a ser dividido
tx <- c("Esta es la primera frase.\nEsta es la segunda frase.")

## Divide utilizando salto de linea
stri_split(tx, regex = "\n")
[[1]]
[1] "Esta es la primera frase." "Esta es la segunda frase."
Código
## Divide utilizando espacio
stri_split(tx, regex = " ")
[[1]]
[1] "Esta"         "es"           "la"           "primera"      "frase.\nEsta"
[6] "es"           "la"           "segunda"      "frase."      

¿Ya has intentado comparar los términos con o sin tildes? Acentuación y puntuación representan obstáculos comunes para la comparación de textos, especialmente cuando se aplican técnicas como la de bag-of-words. En estos casos, aquí, aqui y aquí. son consideradas palabras distintas. Para ello, hace falta remover la puntuación y los acentos para poder compararlas y encontrar su semejanza.

Código
# Carga el paquete
library(stringi)

## Declara el texto
tx <- c("José, María y Elena quieren ir a la fiesta de ensueño. Pero, ¿de qué fiesta hablas, Pepe?")

# Elimina la puntuacion
stri_replace_all(tx, regex = "[:punct:]","")
[1] "José María y Elena quieren ir a la fiesta de ensueño Pero de qué fiesta hablas Pepe"
Código
# Elimina todos los acentos
stri_trans_general(tx, "Latin-ASCII")
[1] "Jose, Maria y Elena quieren ir a la fiesta de ensueno. Pero, ?de que fiesta hablas, Pepe?"

Estos ejemplos constituyen una pequeña introducción a las expresiones regulares. Hay un mundo de referencias a ser exploradas y hace falta tener siempre a mano un conjunto de chuletas para ayudarnos a buscar patrones de texto dependiendo del tipo de texto que estamos utilizando en cada momento.

Referencias adicionales

Existe un enorme material disponible sobre expresiones regulares, os recomiendo los siguientes:

  1. Wickam - “Strings” En R for Data Science

  2. “Regular Expressions” en la documentación del paquete stringr

  3. “Regular Expressions” en el laboratorio LADAU

También vale la pena consultar las referencias de los dos paquetes más importantes para la manipulación de datos en R, el stringr y el stringi:

  1. stringi: Fast and Portable Character String Processing in R

  2. stringr

11.4.3 Ejemplos de textos reales

En esta sección realizaremos la limpieza y extracción de datos de los textos empleados en los ejemplos anteriores de web scraping y de lectura de PDFs. Utilizaremos, primero, los libros en español contenidos en la página del Proyecto Gutenberg y, segundo, los decretos presidenciales de Paraguay.

11.4.3.1 Libros del Proyecto Gutenberg

Una breve inspección a los libros contenidos en el Proyecto Gutenberg revela que, tanto al principio como al final de cada archivo, se pueden encontrar textos descriptivos en inglés relativos a la licencia o otros metatados. Antes de iniciar cualquier análisis, por lo tanto, resultaría muy útil remover estos pasajes de los originales para que solo contuvieran los textos en español.

El primer paso consiste en abrir los archivos y buscar indicadores claros y sistemáticos que puedan servir para automatizar la limpieza de los archivos. Después de una rápida búsqueda, hemos podido identificar dos frases que marcan de forma explícita el inicio y el fin del texto en español. Casi todos los libros empiezan con ““*** START OF THIS PROJECT GUTENBERG EBOOK” y terminan con “End of the Project Gutenberg EBook”. Sin embargo, ni todos contienen tales textos. De ese modo, el algoritmo debe buscar esas dos frases para delimitar el texto que seguirá y eliminar los pasajes en inglés. También debe mantener todo el texto en el caso de que no se encuentre ninguna de las dos o solo la frase inicial o final.

El código abajo lleva a cabo la tarea mencionada:

Código
# Extrae los textos de todos los archivos en la carpeta
gt <- readtext("../Text_Classify/Data/Source/Scraping/Gut_txt/Archivos/")

# Carga el paquete
library(stringi)

# Define el texto que identifica el inicio del texto en español
st <- "START OF TH"

# Define el texto que identifica el fin del texto en español
ed <- "End of the Project Gutenberg EBook"

# Para cada texto
for(i in 1:nrow(gt)){

  # Informa el texto
  print(i)
  
  # Divide el texto en líneas
  tx <- stri_split_lines(gt$text[i])
  
  # Convierte en un largo vector de texto
  tx <- unlist(tx)
  
  # Identifica la posicion INICIAL del texto en español
  nst <- grep(st, tx, fixed = F, ignore.case = F)
  
  # Identifica la posición FINAL del texto en español
  ned <- grep(ed, tx, fixed = F, ignore.case = T)
  
  # Si no encuentra la identificación del inicio
  if(length(nst)==0){
    
    # Define como inicio la línea 0
    nst <-0
    }

  # Si no encuentra la identificacion del final
  if(length(ned)==0){
    
    # Define como final la última línea más 1
    ned <- length(tx)+1
  }
  
  # Selecciona solo el texto deste la posición de inicio
  # más una línea y la de final menos una línea
  tx <- tx[(nst+1):(ned-1)]
  
  # Busca el titulo en mayusculas
  n <- which(stri_detect(tx, regex = "[A-Z]{2,}\\s+[A-Z]{2,}")==T)[1]
  
  # Si encuentra un titulo en mayusculas, lee desde la posicion
  # del titulo encontrado
  if(! is.na(n)) tx <- tx[n:length(tx)]

  # Remueve la división en líneas
  tx <- paste(tx, collapse = " ")
  
  # Reemplaza dos espacios por un salto de línea
  # Reproduce los párrafos originales
  tx <- gsub("\\s{2,}","\n",tx, fixed = F)

  # Actualiza el texto en la base de datos
  gt$text[i] <- tx

}

11.4.3.2 Decretos presidenciales de Paraguay

En el último ejemplo de esta parte, seleccionaremos algunos datos clave para clasificar los decretos presidenciales de Paraguay: el órgano a que se refiere el decreto (en general un ministerio), la exposición de motivos o el resumen del mismo y el texto de sus artículos.

El código abajo aplica uno o más estrategias introducidas anteriormente para llevar a cabo dicha tarea.

Código
# Extrae los textos de todos los archivos en la carpeta
gt <- readtext("../Text_Classify/Data/Source/Scraping/PDFs/")

# Selecciona solo los PDFs
gt <- gt[grep(".pdf",gt$doc_id),]

# Crea tres variables nuevas
gt$organo <- NA    # Órgano a que se refiere
gt$motivo <- NA    # Motivo expuesto
gt$teor <- NA      # Artículos del decreto

# Para cada decreto
for(i in 1:nrow(gt)){

  # Divide los textos en líneas
  tx <- stri_split_lines1(gt$text[i])

  # Elimina los espacios en blanco
  tx <- sapply(tx, trimws, USE.NAMES = F)

  # Elimina los múltiples espacios en blanco
  tx <- gsub("\\s{2,}"," ", tx)
  
  # Prepara las partes del texto para que
  # se dividan según un salto de línea
  tx <- gsub("VISTO","  VISTO", tx)
  tx <- gsub("Asunc","  Asunc", tx)
  tx <- gsub("DECRETA:","  DECRETA: ", tx)
  
  # Elimina las líneas
  tx <- paste(tx, collapse = " ")
  
  # Reemplaza los dobles espacios por 
  # saltos de línea
  tx <- gsub("\\s{2,}","\n",tx)
  
  # Vuelve a dividir, pero ahora en
  # párrafos consistentes
  tx <- stri_split_lines1(tx)
  
  # Averigua si contiene la expresión cexter
  ce <- grep("cexter",tolower(tx), fixed = T)

  # En caso positivo, la elimina
  if(length(ce)>0){
    tx <- tx[-ce]
  }
  
  # Encuentra la posicion de los motivos
  # en el texto
  mo <- tx[grep("POR EL CUAL", tx)[1]]
  
  # Encuentra la información sobre el órgano
  pres <- tolower(tx[grep("PRESIDEN", tx)[1]])
  
  # Elimina la presidencia de la identificación
  # del órgano o ministerio
  pres <- gsub("presidencia de la república del paraguay","", pres)
  pres <- trimws(pres)

  # Encuentra el texto de los artículos
  teor <- tx[(grep("DECRETA:", tx)+1):length(tx)]
  
  # Lo convierte en un párrafo
  teor <- paste(teor, collapse = "\n")

  # Si queda texto de ruido, lo elimina
  teor <- gsub("NA\nDECRETA: ","", teor, fixed = T)
  
  # Atribuye cada información a su respectiva
  # variable en la base de datos
  gt$organo[i] <- pres
  gt$motivo[i] <- mo
  gt$teor[i] <- teor
  
}

# Visualiza los resultados
reactable(gt, wrap = F, resizable = T, defaultPageSize = 10)